/* eslint-disable @typescript-eslint/no-explicit-any */
import {
	ValidRule,
	EqualRule,
	MinRule,
	MaxRule,
	PatternRule,
	MismatchRule,
	LogicRule,
	string,
	ValueRule,
	RangeRule,
	number,
	boolean,
	DateRule,
	BetweenRule,
	date,
	array,
	item,
	object,
	prop,
	PrimitiveRule,
	UnionRule,
	NeedRule,
	union,
	need,
	unknown,
} from '.';

let stack: ValidRule[] | undefined;

function errCtx(fn: string): never {
	throw new Error(`"${fn}()" call must be invoked in "define()" context!`);
}

/**
 * String type
 * @param rules String rules
 * @returns String type
 */
export function str(
	...rules: (EqualRule | MinRule | MaxRule | PatternRule | MismatchRule | LogicRule)[]
): string | undefined | null {
	if (stack) {
		stack.push(string(...rules));
		return undefined;
	} else {
		errCtx(str.name);
	}
}

/**
 * Number type
 * @param rules Number rules
 * @returns Number type
 */
export function num(
	...rules: (EqualRule | ValueRule | RangeRule | MismatchRule | LogicRule)[]
): number | undefined | null {
	if (stack) {
		stack.push(number(...rules));
		return undefined;
	} else {
		errCtx(num.name);
	}
}

/**
 * Boolean type
 * @param rules Boolean rules
 * @returns Boolean type
 */
export function bool(...rules: (EqualRule | MismatchRule | LogicRule)[]): boolean | undefined | null {
	if (stack) {
		stack.push(boolean(...rules));
		return undefined;
	} else {
		errCtx(bool.name);
	}
}

/**
 * Date type
 * @param rules Date rules
 * @returns Date type
 */
export function dat(
	...rules: (EqualRule | DateRule | BetweenRule | MismatchRule | LogicRule)[]
): string | undefined | null {
	if (stack) {
		stack.push(date(...rules));
		return undefined;
	} else {
		errCtx(dat.name);
	}
}

/**
 * Array type
 * @param rules Array rules
 * @returns Array type
 */
export function arr<T>(_: T, ...rules: (MinRule | MaxRule | MismatchRule | LogicRule)[]): T[] | undefined | null {
	if (stack) {
		const r = stack.pop();
		if (!r) throw new Error('invalid array definition: must specify sub type');
		const a = array(item(r), ...rules);
		stack.push(a);
		return undefined;
	} else {
		errCtx(arr.name);
	}
}

type Obj<T> = {
	[K in keyof T as undefined extends T[K] ? K : never]?: T[K];
} & {
	[K in keyof T as undefined extends T[K] ? never : K]: T[K];
};

/**
 * Object type
 * @param val Object type
 * @returns Object type
 */
export function obj<T extends Record<string, unknown>>(val: T): Obj<T> {
	if (stack) {
		const keys = Object.keys(val);
		if (keys.length > stack.length) throw new Error('All object field must provide type definition!');
		const s = stack;
		const r = object(...keys.map((k, i) => prop(k, s[s.length - keys.length + i])));
		stack.length -= keys.length;
		stack.push(r);
		return val;
	} else {
		errCtx(obj.name);
	}
}

/**
 * Unknown type
 * @returns Unknown type
 */
export function unk(): any {
	if (stack) {
		stack.push(unknown());
		return undefined;
	} else {
		errCtx(unk.name);
	}
}

/**
 * Union type
 * @param item Any primitive type
 * @returns Union type
 */
export function uni<T extends unknown[]>(...item: T): T[number] {
	function flatten(rules: ValidRule[]): PrimitiveRule[] {
		const subRules = rules
			.filter((s): s is UnionRule => s.type === 'union')
			.map((s) => s.rules)
			.flat(1)
			.filter((r): r is PrimitiveRule => r.type !== 'mismatch');
		const reqRules = rules
			.filter((s): s is NeedRule => s.type === 'need')
			.map((s) => s.rule)
			.filter((r): r is PrimitiveRule | UnionRule => !!r);
		const flattened = reqRules.length > 0 ? flatten(reqRules) : [];
		const primitiveRules = rules.filter((s): s is PrimitiveRule => s.type !== 'need' && s.type !== 'union');
		return [...subRules, ...flattened, ...primitiveRules];
	}
	if (stack) {
		if (stack.length < item.length) {
			throw new Error('invalid union types');
		}
		const pushed = stack.slice(stack.length - item.length);
		const unionRule = union(...flatten(pushed));
		stack.length -= item.length;
		stack.push(unionRule);
		return undefined as any;
	} else {
		errCtx(uni.name);
	}
}

/**
 * Required type
 * @param _
 * @returns Required type
 */
export function req<T>(_: T): NonNullable<T> {
	if (stack) {
		let r = stack.pop();
		if (!r) throw new Error('invalid reqire definition: must specify sub type');
		if (r.type === 'need') {
			r = r.rule ?? undefined;
		}
		stack.push(need(r ? r : null));
		return undefined as any;
	} else {
		errCtx(req.name);
	}
}

export interface TypeDefine<T> {
	type: T;
	rule: ValidRule;
	def(): T;
}

export function define<T>(fn: () => T): TypeDefine<T> {
	stack = [];
	const o = fn();
	if (stack.length !== 1) throw new Error('Invalid type definition');
	const r = stack[0];
	const result = {
		type: o,
		rule: r,
		def() {
			if (stack) {
				stack.push(r);
				return o;
			} else {
				errCtx(fn.name);
			}
		},
	};
	stack = undefined;
	return result;
}
